1 module widgets;
2 
3 import models;
4 
5 import jupyter.wire.kernel;
6 import jupyter.wire.message;
7 import std.json : JSONValue, parseJSON;
8 import std.meta;
9 
10 interface IWidget {
11     void close(scope IoPubMessageSender sender) @safe;
12     void update(scope IoPubMessageSender sender, JSONValue newState) @safe; // process change from the backend (will send change to frontend as well)
13     void onUpdate(in JSONValue newState, in JSONValue buffer_paths) @safe; // process change from the frontend
14     void onCustomMessage(scope IoPubMessageSender sender, in JSONValue content) @safe;
15     void onRequestState(scope IoPubMessageSender sender) @safe;
16     void onRemove(scope IoPubMessageSender sender, in JSONValue data) @safe;
17     void display(scope IoPubMessageSender sender) @safe;
18     string getCommId() @safe;
19 }
20 
21 enum WidgetProtocolMetadata = parseJSON(`{"version":"2.0.0"}`);
22 
23 // Each Widget is templatized on a model. A model may contain reference to other models, which each need an instantiated widget.
24 // This helper struct initializes all referenced widgets.
25 // e.g. many models reference a LayoutModel or a StyleModel
26 struct ReferenceWidgets(T) {
27     alias makeWidget(T) = Widget!T;
28     alias Models = getReferenceModels!T.Types;
29     alias Names = getReferenceModels!T.Names;
30     alias Widgets = staticMap!(makeWidget, Models);
31     static foreach(idx, W; Widgets) {
32         mixin("W "~Names[idx]~";");
33     }
34     void initialize(ref T state, scope IoPubMessageSender sender) @safe {
35         static foreach (idx, Widget; Widgets) {{
36                 auto widget = new Widget(sender);
37                 mixin(Names[idx]~" = widget;");
38                 __traits(getMember, state, Names[idx]) = widget.commId.makeReference();
39             }}
40     }
41 }
42 
43 class Widget(T) : IWidget {
44     private string commId;
45     private ReferenceWidgets!T referenceWidgets;
46     T state;
47 
48     this(scope IoPubMessageSender sender) @safe {
49         import std.uuid: randomUUID;
50         this(sender, randomUUID.toString);
51     }
52 
53     this(scope IoPubMessageSender sender, string commId) @safe {
54         this(sender, commId, T());
55         sender(commOpenMessage(commId, "jupyter.widget", this.state.serialize(), WidgetProtocolMetadata));
56     }
57 
58     this(scope IoPubMessageSender sender, string commId, T state) @safe {
59         this.commId = commId;
60         this.state = state;
61         referenceWidgets.initialize(this.state, sender);
62     }
63 
64     override void close(scope IoPubMessageSender sender) @safe {
65         sender(commCloseMessage(commId));
66     }
67 
68     override void update(scope IoPubMessageSender sender, JSONValue newState) @safe {
69         this.state.update(newState, JSONValue());
70         JSONValue data;
71         data["state"] = newState;
72         sender(commMessage(commId, data, WidgetProtocolMetadata));
73     }
74 
75     override void onUpdate(in JSONValue newState, in JSONValue buffer_paths) @safe {
76         this.state.update(newState, buffer_paths);
77     }
78 
79     override void onRequestState(scope IoPubMessageSender sender) @safe {
80         auto data = this.state.serialize();
81         data["method"] = "update";
82         sender(commMessage(commId, data, WidgetProtocolMetadata));
83     }
84 
85     override void onCustomMessage(scope IoPubMessageSender sender, in JSONValue content) @safe {
86     }
87 
88     override void onRemove(scope IoPubMessageSender sender, in JSONValue data) @safe {
89     }
90 
91     override void display(scope IoPubMessageSender sender) @safe {
92         JSONValue widgetView;
93         widgetView["model_id"] = commId;
94         widgetView["version_major"] = 2;
95         widgetView["version_minor"] = 0;
96         JSONValue data;
97         data["application/vnd.jupyter.widget-view+json"] = widgetView;
98         sender(displayDataMessage(data));
99     }
100 
101     override string getCommId() @safe {
102         return commId;
103     }
104 }
105 
106 @safe unittest {
107     import models : FloatSliderModel;
108     import std.algorithm : startsWith;
109     auto w = new Widget!FloatSliderModel((Message){});
110     assert(w.state.style.startsWith("IPY_MODEL_"));
111     assert(w.state.layout.startsWith("IPY_MODEL_"));
112 }
113 
114 IWidget constructWidget(in string commId, in JSONValue data, scope IoPubMessageSender sender) @safe {
115     import models : AllModels;
116     import std.format : format;
117 
118     const modelModule = data["_model_module"].str;
119     const modelName = data["_model_name"].str;
120     const viewModule = data["_view_module"].str;
121     const viewName = data["_view_name"].str;
122 
123     static foreach(Model; AllModels) {
124         if (Model._model_module == modelModule &&
125             Model._model_name == modelName &&
126             Model._view_module == viewModule &&
127             Model._view_name == viewName) {
128             Model model;
129             model.update(data, JSONValue());
130             return new Widget!(Model)(sender, commId, model);
131         }
132     }
133 
134     throw new Exception("Cannot construct widget module %s:%s with view %s:%s".format(modelModule, modelName, viewModule, viewName));
135 }
136 
137 @safe unittest {
138     import std.json : parseJSON;
139     bool isCalled = false;
140 
141     auto widget = constructWidget("abcd", parseJSON(`{"_model_module":"@jupyter-widgets/controls","_model_name":"FloatSliderModel","_view_module":"@jupyter-widgets/controls","_view_name":"FloatSliderView","max":250.0}`), (Message msg){});
142 
143     assert(widget !is null);
144     widget.onRequestState((Message msg){
145             if (msg.content["comm_id"].str == "abcd") {
146                 isCalled = true;
147                 assert(msg.content["data"]["state"]["max"].floating == 250.0);
148             }
149         });
150     assert(isCalled == true);
151 }
152 
153 @safe unittest {
154     import std.json : parseJSON;
155     bool isCalled = false;
156 
157     auto widget = constructWidget("abcd", parseJSON(`{"_model_module":"@jupyter-widgets/controls","_model_name":"FloatSliderModel","_view_module":"@jupyter-widgets/controls","_view_name":"FloatSliderView","max":250.0}`), (Message msg){});
158 
159     assert(widget !is null);
160     widget.update((Message msg){
161             if (msg.content["comm_id"].str == "abcd") {
162                 isCalled = true;
163                 assert(msg.content["data"]["state"]["max"].floating == 260.0);
164             }
165         }, parseJSON(`{"max": 260.0}`));
166     assert(isCalled == true);
167 }
168